package gaml.text;

import gaml.core.GamlConfig;
import gaml.error.GamlException;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;


/**
 * yaml code in text representation
 * @author shnireck
 *
 */
public class YamlText {
	public static final int READBUFFERLENGTH = 4096;
	public static final String errorLineBeyondRangeError = "End of text reached at position [%d]";
	public static final String errorAppendToEmpty = "Can't append to empty text";
	
	String lb = "\n";
	String identString = "\t";
	int shift = 0;
	
	ArrayList <YamlLine> lines = new ArrayList <YamlLine> ();
	
	int current = 0;
	YamlLine tail = null;
	
	/**
	 * simple yaml text representaion
	 */
	public YamlText () { init ();	}
	
	/**
	 * yaml text for specifid settings
	 * @param config serializer settings
	 */
	public YamlText (GamlConfig config, int shift) {
		init ();
		
		lb = config.getPrinterSettings ().getLineBreak ();
		identString = config.getPrinterSettings ().getIdentationString ();
		this.shift = shift;
	}
	
	/**
	 * create new yaml text object
	 * from a part of exsisted
	 * @param parent yaml object from which to create new one
	 * @param line yaml object line, from which a part to create from starts
	 * @throws GamlException if yaml line format incorrect
	 */
	public YamlText (YamlText parent, int line) throws GamlException {
		setLineBreak (parent.getLineBreak ());
		setIdentationString (parent.getIdentationString ());
		
		int shift = parent.getIdentation (line);
		while (! parent.isEnd (++line))
			if (shift >= parent.getIdentation (line)) break;
			else lines.add (new YamlLine (parent.getValue (line), identString, shift));
	}
	
	
	/**
	 * get line brake symbol sequence
	 * default is \n (Lf, unix sequence)
	 * @return line break value
	 */
	public String getLineBreak () { return lb; }
	
	/**
	 * define line brake symbol sequence
	 * default is \n (Lf, unix sequence)
	 * @param lb new line brake sequence
	 * @return line break value
	 */
	public YamlText setLineBreak (String lb) { this.lb = lb; return this; }
	
	/**
	 * get identation sequence for file
	 * @return ident
	 */
	public String getIdentationString () { return identString; }
	
	/**
	 * define identation sequence for file
	 * @param ident identation sequence
	 * @return ident
	 */
	public YamlText setIdentationString (String ident) { this.identString = ident; return this; }
	
	/**
	 * identation for specified line
	 * @param line line number, start from 0
	 * @return identation size
	 * @throws GamlException 
	 */
	public int getIdentation (int line) throws GamlException {return getLine (line).getIdent (); }
	
	/**
	 * identation for current line
	 * @return identation size
	 * @throws GamlException 
	 */
	public int getIdentation () throws GamlException { return getLine ().getIdent (); }
	
	/**
	 * specific yaml line tag
	 * @param line nuber of line to see
	 * @return specified tag
	 * @throws GamlException 
	 */
	public String getTag (int line) throws GamlException { return getLine (line).getTag (); }
	
	/**
	 * current yaml line tag
	 * @return line tag
	 * @throws GamlException 
	 */
	public String getTag () throws GamlException { return getLine ().getTag (); }
	
	/**
	 * specific yaml line anchor
	 * @param line nuber of line to see
	 * @return specified anchor
	 * @throws GamlException 
	 */
	public String getAnchor (int line) throws GamlException { return getLine (line).getAnchor (); }
	
	/**
	 * current yaml line anchor
	 * @return line anchor
	 * @throws GamlException 
	 */
	public String getAnchor () throws GamlException { return getLine ().getAnchor (); }
	
	/**
	 * specific yaml line text
	 * @param line nuber of line to see
	 * @return specified
	 * @throws GamlException 
	 */
	public String getValue (int line) throws GamlException { return getLine (line).getValue (); }
	
	/**
	 * current yaml line text
	 * @return line text
	 * @throws GamlException 
	 */
	public String getValue () throws GamlException { return getLine ().getValue (); }
	
	
	/** get yaml line with specified number
	 * @param line number of line to get
	 * @return yaml line from specified number
	 */
	public YamlLine getLine (int line) throws GamlException {
		if (isEnd ())
			throw new GamlException (String.format (errorLineBeyondRangeError, current));
		return lines.get (line);
	}
	
	/** gettin current line
	 * @return current yaml line
	 * @throws GamlException 
	 */
	public YamlLine getLine () throws GamlException { return (null == tail ? getLine (current) : tail); }
	
	/**
	 * adds new line to the end of the lines list
	 * @param line yaml line to add
	 * @return host object
	 */
	public YamlText addLine (YamlLine line) { lines.add (line); return this; }
	
	/**
	 * adds lines from anothe yaml text object
	 * to the end this yaml text object
	 * @param text yaml text to add 
	 * @return host object
	 */
	public YamlText addText (YamlText text) { lines.addAll (text.lines); return this; }
	
	/**
	 * replace element at specified position with specified new element
	 * @param where position to set
	 * @param line yaml line to set
	 * @return host object
	 */
	public YamlText setLine (int where, YamlLine line) { lines.set (where, line); return this; }
	
	/**
	 * go to the next line if any
	 * @return host object
	 */
	public YamlText next () {
		if (null != tail) tail = null;
		else if (! isEnd ()) current++;
		
		return this;
	}
	
	/**
	 * define a part of previoce yaml line
	 * wich represents a begining of node
	 * for example, we have:
	 *  
	 * ?map1:- seq1
	 *    - seq2
	 *    - seq3
	 *    
	 *  current line is second ("- seq2")  
	 *  tail would be "- seq1"
	 * @param tailText string tail value
	 * @return host object
	 * @throws GamlException 
	 */
	public YamlText setTail (String tailText) throws GamlException {
		tail = (tailText.isEmpty () ? null : new YamlLine (tailText, identString, - getIdentation () - 1));
		
		if (! isEnd ()) { 
			current++;
			if ((null != tail) && (tail.getValue ().isEmpty ())) { getLine (current).setTag (tail.getTag ()); tail = null; }
		}
		
		return this;
	}
	
	/**
	 * check if tail is set for this text object
	 * @return true if tail is set, false of not
	 */
	public boolean hasTail () { return (null != tail); }
	
	/**
	 * line number is beyond text lines range
	 * @param line some line
	 * @return true if line is and line, false if not
	 */
	public boolean isEnd (int line) { return line == lines.size (); }
	
	/**
	 * current line number is beyond text lines range
	 * @return true if current line is last, else false
	 */
	public boolean isEnd () {return (null == tail && isEnd (current));}
	
	
	/**
	 * reading yaml text from file
	 * @param file file to read yaml from
	 * @return host object
	 * @throws IOException if file not found or could not be read
	 * @throws GamlException if yaml line format incorrect
	 */
	public YamlText read (File file) throws IOException, GamlException {
		char [] buffer = new char [READBUFFERLENGTH];
		StringBuilder yaml = new StringBuilder ();
			
		BufferedReader reader = new BufferedReader (new FileReader (file));
		while (-1 != reader.read (buffer, 0, READBUFFERLENGTH))
			yaml.append (buffer);
		
		return this.read (yaml.toString ());
	}
	
	/**
	 * parse string text into separate lines
	 * and determine identation for each of them
	 * @param string text to parse
	 * @return host object
	 * @throws GamlException if yaml line format incorrect
	 */
	public YamlText read (String string) throws GamlException {
		lines.clear ();
		String [] strings = string.split (lb);
		for (String text: strings) {
			YamlLine line = new YamlLine (text, identString, 0);
			if (! line.isEmpty ()) lines.add (line);
		}
		
		return this;
	}
	
	/**
	 * yaml text parsed into single string
	 * @return string of text
	 */
	public String getText () {
		StringBuilder sb = new StringBuilder ();
		for (YamlLine line: lines) {
			for (int i=0; i < (line.getIdent () - shift); ++i)
				sb.append (this.identString);
			sb.append (getTextLine (line)).append (this.getLineBreak ());
		}
		return sb.toString ();
	}
	
	/**
	 * adds block of serialized yaml text as subblock to this text
	 * @param text subblock yaml text
	 * @return this text object
	 * @throws GamlException 
	 */
	public YamlText appendText (YamlText text) throws GamlException {
		if (0 == this.getSize ()) throw new GamlException (errorAppendToEmpty);
		if (0 == text.getSize ()) return this; // nothing to append - appending nothing
		
		YamlLine last = this.getLine (this.getSize () - 1);
		last.setValue (String.format ("%s %s", last.getValue (), getTextLine (text.getLine ())));
		
		while (! text.next ().isEnd ()) this.addLine (text.getLine ());
		
		return this;
	}
	
	/**
	 * transforms yaml line to yaml text string
	 * representation's string builder
	 * @param line yaml line to add to yaml text string representation
	 * @return yaml line string representation
	 */
	protected String getTextLine (YamlLine line) {
		StringBuilder sb = new StringBuilder ();

		sb.append (line.getTag ().isEmpty () ? "" : String.format ("!%s ", line.getTag ()))
		.append (line.getAnchor ().isEmpty () ? "" : String.format ("&%s ", line.getAnchor ()))
		.append (line.getValue ());
		
		return sb.toString ();
	}
	
	/**
	 * number of lines parsed
	 * @return number of lines
	 */
	public int getSize () { return lines.size (); }
	
	/**
	 * get current line number
	 * @return number for current line
	 */
	public int getCurrent () { return current; }
	
	protected void init () {
		current = 0;
		tail = null;
		
		lines = new ArrayList <YamlLine> ();
	}
}